简要概念
依赖注入
方式1:构造器注入。
创建应用组件之间协作行为通常称为装配(wiring)。
可以用xml或java实现。
Spring通过应用上下文(Application Context)装载bean的定义并把它们组装起来。Spring应用上下文全权负责对象的创建和组装。Spring自带了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置。
对于XML使用CLassPathXMLApplication,对于Java配置,使用AnnotationConfigApplicationContext。
应用切面
DI能够让相互协作的软件组件保持松散耦合,而面向切面编程(AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。
可以把切面想象为覆盖在很多组件上的一个外壳。应用是由那些实现各自业务功能的模块组成的。
可以在xml中将某个bean声明为一个切面。
使用模板消除样板式代码
比如jdbcTemplate。
容纳Bean
在基于Spring的应用中,应用对象生存于Spring容器(container)中。Spring容器负责创建对象,装配它们,配置它们并管理它们的整个生命周期,从生存到死亡。
容器是 Spring框架的核心。 Spring容器使用DI管理构成应用的组件,它会创建
相互协作的组件之间的关联。毫无疑问,这些对象更简单干净,更易于理解,更易于重用并且更易于进行单元测试。
spring自带了多种容器实现,归为两种类型,bean工厂与应用上下文。bean工厂对于大多数应用来说往往太低级,因此,应用上下文要比bean工厂更受欢迎。
使用应用上下文
无论是从文件系统中装载应用上下文还是从类路径下装载应用上下文,将bean加载到bean工厂的过程都是相似的。区别在于前者在指定的文件系统路径下查找xml文件,后者是在所有的类路径(包括JAR文件)下查找xml文件。AnnotationConfigApplicationContext通过一个配置类加载bean。
应用上下文准备就绪之后,就可以调用上下文的getBean()方法从Spring容器中获取bean。
bean的生命周期
在传统的Java应用中,bean的生命周期很简单。使用Java关键字new进行bean实例化,然后该bean就可以使用了。一旦该bean不再被使用,则由Java自动进行垃圾回收。相比之下,Spring容器中的bean的生命周期就显得相对复杂多了。
Spring核心容器
容器是Spring框架最核心的部分,它管理着Spring 应用中bean的创建、配置和管理。在该模块中,包括了Spring bean 工厂,它为Spring提供了DI的功能。基于bean工厂,我们还会发现有多种Spring应用上下文的实现,每一种都提供了配置Spring的不同方式。所有的Spring模块都构建于核心容器之上,当配置应用时,其实隐式地使用了这些类。
Spring的AOP模块
在AOP模块中,Spring 对面向切面编程提供了丰富的支持。这个模块是Spring 应用系统中开发切面的基础。与DI一样, AOP可以帮助应用对象解耦。借助于AOP,可以将遍布系统的关注点(例如事务和安全)从它们所应用的对象中解耦出来。
装配Bean
在Spring中,对象无需自己查找或创建与其所关联的其他对象。容器负责把需要相互协作的对象引用赋予各个对象。创建应用对象之间协作关系的行为称为装配,也是依赖注入的本质。
Spring配置的可选方案
Spring提供三种主要的装配机制:
- 在XML中进行显式配置;
- 在java中进行显式配置;
- 隐式地bean发现机制和自动装配。
作者建议尽可能地使用自动配置机制,显式配置越少越好,当必须要显式配置bean时,推荐使用类型安全并且比XML更加强大的JavaConfig。最后,只有想使用便利的XML命名空间,并且在JavaConfig中没有同样的实现时,才应该使用XML。
自动化装配bean
Spring从两个角度来实现自动化装配:
- 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。
- 自动装配(autowiring):Spring自动满足bean之间的依赖。
组件扫描和自动装配组合在一起就能发挥出强大的威力,能够将显式配置降低到最少。
创建可被发现的bean
@Component注解表明该类会作为组件类,并告知Spring要为这个类创建bean。
不过组件扫描默认是不启用的,还需要显式配置一下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean。
@ComponentScan注解可以在Spring中启用组件扫描。如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包,Spring将会扫描这个包以及包下的所有子包,查找带有@Component注解的类并且在Spring中自动为其创建一个bean。
还可以使用XML来启动组件扫描。
还是基于Java的配置用的多,喜好问题。
一个实例:
@ContextConfiguration注解说明需要在CDPlayerConfig中加载配置。因为CDPlayerConfig类中包括了@ComponentScan,所有带有@Component注解的类都会创建为bean。
为组件扫描的bean命名
Spring应用上下文中所有的bean都会给定一个ID,就是将类名的第一个字母变为小写。也可以使用@Component为bean设置不同的ID。
设置组件扫描的基础包
默认扫描的是配置类所在的包作为基础包(base package)来扫描组件。
可以指定多个包,扫描多个包。但是basePackages有一个问题是其是字符串,所以当包不存在时IDE也不会第一时间报错。可以设置basePackageClasses,设置数组中包含了类,这些类所在的包将会作为组件扫描的基础包。
通过为bean添加注解实现自动装配
自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其他bean。可以借助@Autowired注解进行自动装配。
@Autowired注解不仅能够用在构造器上,还能用在属性的Setter方法上。如:
在Spring初始化bean之后,会尽可能去满足bean的依赖。不管是构造器,Setter还是其他的方法,Spring都会尝试满足方法参数上所声明的依赖,假设有且只有一个bean匹配依赖的话,那么这个bean将会被装配进来,如果没有匹配的bean,那么在应用上下文创建时,Spring会抛出一个异常,也可以将@Autowired的required属性设置为false避免抛异常,当设置为false时,Spring会尝试执行自动装配,但是如果没有匹配的bean的话,Spring将会让这个bean处于未装配的状态,此时需要谨慎对待这个bean,如果代码没有进行null检查的话,这个处于未装配的属性有可能会出现NullPointerException。
如果有多个bean都能满足依赖关系的话,Spring将会抛出一个异常,表明没有明确指定要选择哪个bean进行自动装配。
@Autowired是Spring特有的注解。如果你不愿意在代码中到处使用Spring的特定注解来完成自动装配任务的话,可以考虑将其替换为@Inject。
@Inject注解来源于Java依赖注入规范,该规范同时还定义了@Named注解。在自动装配中,Spring 同时支持@Inject和@Autowired. 尽管@Inject和@Autowired之间有着一些细微的差别, 但是在大多数场景下,它们都是可以互相替换的。
通过Java代码装配bean
尽管在很多场景下通过组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通,因此需要明确配置Spring。比如说,想要将第三方库中的组件装配到应用中,在这种情况下,是没有办法在它的类上添加@Component和@Autowired注解的,因此就不能使用自动化装配的方案了。
此时就需要采用显示装配的方式,分为Java和XML。在进行显式配置时,JavaConfig是更好的方案,因为其更为强大、类型安全并且对重构友好。因为它就是Java代码,就像应用程序中的其他Java代码一样。同时JavaConfig与其他的Java代码又有所区别,JavaConfig是配置代码,这意味着它不应该包含任何业务逻辑,JavaConfig也不应该侵入到业务逻辑代码之中,通常会将JavaConfig放到单独的包中,使它与其他的应用程序逻辑分离开。
创建配置类
创建JavaConfig类的关键在于为其添加@Configuration注解,@Configuration注解表明这个类是一个配置类, 该类应该包含在Spring应用上下文中如何创建bean的细节。对于显式配置,将@ConponentScan注解移除。此时那些bean不会被发现,配置类也没有作用了。
声明简单的bean
要在JavaConfig中声明bean,需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解。
@Bean注解告诉Spring方法返回一个对象,该对象要注册为Spring应用上下文中的bean。方法体中包含了最终产生bean实例的逻辑。
借助JavaConfig实现注入
在JavaConfig中装配bean的最简单方式就是引用创建bean的方法。
通过调用方法来引用bean令人困惑,还有一种理解起来更为简单的方式:
这里使用构造器实现了DI功能,但是完全可以使用其他风格的DI配置,比如可以通过Setter方法注入:
带有@Bean注解的方法可以采用任何必要的Java功能来产生bean实例。构造器和Setter方法只是@Bean方法的两个简单样例。
通过XML装配bean
在装配bean时还可以选择XML,现在已经不太合乎大家的心意了,但是在Spring中已经有很长的历史了。
创建XML配置规范
在XML配置中,意味着要创建一个XML文件。并且要以
声明一个简单的
借助构造器注入初始化bean
导入和混合配置
关于混合配置,首先不要在意装配的bean来自哪里,自动装配会考虑到Spring容器中所有bean,不管是在JavaConfig或XML中声明还是组件扫描获得的。
在JavaConfig中引用XML配置
可以有一个更高级别的配置类,在类中使用@Import将两个配置类组合在一起:
假如配置在了XML中,则Spring可以使用@ImportResource注解加载。
两个bean,配置在JavaConfig中以及配置在XML中都会被加载到Spring容器中。
在XML配置中引入JavaConfig
不管使用JavaConfig还是使用XML进行装配,通常都会创建一个根配置(root configuration),这个配置会将两个或更多的装配类和或XML文件组合起来。也会在根配置中启用组件扫描(通过context:component-scan或@ComponentScan)。
高级装配
环境与profile
配置profile bean
要使用profile,首先要讲所有不同的bean定义整理到一个或多个profile之中,将应用部署到每个环境时,要确保对应的profile处于激活(active)的状态。在Java配置中,可以使用@Profile注解指定某个bean属于哪个profile。
@Profile注解应用在了类级别上,它会告诉Spring这个配置类中的bean只有在dev profile激活时才会被创建,如果没有激活的话,那么带有@Bean注解的方法都会被忽略掉。
在XML中配置profile
激活profile
Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default. 如果设置了spring.profiles.active属性的话,那么它的值就会用来确定哪个profile 是激活的。但如果没有设置spring .profiles.active属性的话,那Spring 将会查找spring.profiles.default的值。如果spring.profiles.active 和spring.profiles.default均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在profile中的bean。
有多种方式来设置这两个属性:
1.作为DispatcherServlet的初始化参数;
2.作为Web应用的上下文参数;
3.作为JNDI条目;
4.作为环境变量;
5.作为JVM的系统属性;
6.在集成测试类上,使用@ActiveProfiles注解设置。
条件化的bean
@Conditional注解可以用到带有@Bean注解的方法上,如果给定的条件计算结果为true,就会创建这个bean,否则,这个bean会被忽略。
设置给@Conditional的类可以是任意实现了Condition接口的类型。这个接口实现起来很简单直接,只需提供matches()方法实现即可。如果matches()方法返回true,就会创建带有@Conditional注解的bean,返回false则不会创建这些bean。
通过ConditionContext,可以做到:
AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法还有其他什么注解。借助isAnnotated()方法,我们能够判断带有@Bean注解的方法是不是还有其他特定的注解。借助其他的那些方法,我们能够检查@Bean注解的方法上其他注解的属性。
@Profile注解如下所示:
@Profile本身也使用了@Conditional注解,并且在做出决策的过程中,考虑到了ConditionContext和AnnotatedTypeMetadata中的多个因素。
处理自动装配的歧义性
仅有一个bean匹配所需的结果时,自动装配才是有效的,如果不仅有一个bean能匹配结果的话,这种歧义性会阻碍Spring自动装配属性,构造器参数或方法参数。可以将可选bean中的某一个设为首选(primary)的bean,或者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean。
标示首选的bean
使用@Primary注解
如果是通过Java配置显式声明,应该如下:
使用XML
限定自动装配的bean
Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的限定符都用上后依然存在歧义性,那么可以继续使用更多的限定符来缩小选择范围。
@Qualifier注解是使用限定符的主要方式,它可以与@Autowired和@Inject协同使用,在注入时指定想要注入进去的是哪个bean。
@Qualifier注解所设置的参数就是想要注入的bean的ID。
创建自定义的限定符
自己为bean设置自己的限定符。在bean声明上添加@Qualifier注解。
在注入的地方,只要引用cold限定符就可以了。
通过Java配置显式定义bean时,@Qualifier也可以与@Bean注解一起使用:
使用自定义的限定符注解
比如自定义@Cold注解。
在注入点使用必要的限定符注解进行任意组合,从而将可选范围缩小到只有一个bean满足需求。
通过声明自定义的限定符注解,可以同时使用多个限定符。
bean的作用域
在默认情况下,Spring 应用上下文中所有bean都是作为以单例(singleton) 的形式创建的。也就是说,不管给定的一个bean被注人到其他bean多少次,每次所注入的都是同一个实例。
Spring定义了多种作用域,可以基于这些作用域创建bean,包括:
1.单例( Singleton):在整个应用中,只创建bean的一个实例。
2.原型( Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
3.会话(Session): 在Web应用中,为每个会话创建一个bean实例。
4.请求(Rquest):在Web应用中,为每个请求创建一个bean实例。
这里使用ConfigurableBeanFactory类的SCOPE_ PROTOTYPE 常量设置了原型作用城。当然也可以使用@Scope (“prototype”),但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。
java配置中声明为原型bean则使用:
XML配置bean,使用
使用会话和请求作用域
就购物车bean而言,会话作用域是最为合适的。
proxyMode属性的配置表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。
如果ShoppingCart是接口而不是类的话,这是可以的(最为理想的代理模式)。但如果ShoppingCart是一个具体的类的话, Spring 就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode .TARGET_ CLASS,以此来表明要以生成目标类扩展的方式创建代理。
在XML中声明作用域代理
aop:scoped-proxy是与@Scope注解的proxyMode属性功能相同的SpringXML配置元素。它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLib创建目标类的代理。但是我们也可以将proxy-target-class 属性设置为false,进而要求它生成基于接口的代理。
运行时值注入
当讨论依赖注入的时候,我们通常所讨论的是将一个 bean引用注入到另一个bean的属性或构造器参数中。它通常来讲指的是将一个对象与另一个对象进行关联。
bean装配的另外一个方面指的是将一个值注入到bean的属性或者构造器参数中。Spring提供了两种在运行时求值的方式:
1.属性占位符
2.Spring表达式语言(SpEL)
注入外部的值
处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。
解析属性占位符
Spring一直支持将属性定义到外部的属性的文件中,并使用占位符值将其插入到Spring bean中,占位符的形式为”${…}”
XML配置没有使用任何硬编码的值。
使用Spring表达式语言进行装配
面向切面的Spring
在软件开发中,散布于应用多处的功能被称为横切关注点。这些横切关注点从概念上是与应用的业务逻辑相分离的。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
什么是面向切面编程
如果要重用通用功能的话,最常见的面向对象技术是继承或委托。但是如果在应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系;使用委托可能需要对委托对象进行复杂得调用。切面提供了取代继承和委托的另一种可选方案,在很多场景下更清晰简洁,在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面。
这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。
定义AOP术语
通知(Advice):
在AOP术语中,切面的工作被称为通知。
Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After): 在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning):在目标方法成功执行之后调用通知;
- 异常通知( After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知( Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
连接点(Join point):
切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
切点(Poincut):
如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。
切面(Aspect):
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容一它是什么 ,在何时和何处完成其功能。
引入(Introduction):
引入允许我们向现有的类添加新方法或属性。
织入(Weaving):
织人是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入( load-time weaving, LTW )就支持以这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
Spring对AOP的支持
Spring 提供了4种类型的AOP支持:
- 基于代理的经典Spring AOP;
- 纯POJO切面;
- @AspectJ注解驱动的切面;
- 注入式AspectJ切面( 适用于Spring各版本)。
Spring通知是Java编写的
Spring所创建的通知都是用标准的Java类编写的。可以使用与普通Java开发一样的(IDE) 来开发切面。定义通知所应用的切点通常会使用注解或在Spring配置文件里采用XML来编写,这两种语法对于Java开发者来说都是相当熟悉的。
AspectJ与之相反。虽然AspectJ现在支持基于注解的切面,但AspectJ 最初是以Java语言扩展的方式实现的。这种方式有优点也有缺点。通过特有的AOP语言,可以获得更强大和细粒度的控制,以及更丰富的AOP工具集,但是需要额外学习新的工具和语法。
Spring在运行时通知对象
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法前,会执行切面逻辑。
直到应用需要被代理的bean 时,Spring 才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring 才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织人Spring AOP的切面。
Spring只支持方法级别的连接点
因为Spring基于动态代理,所以Spring只支持方法连接点。这与一些其他的AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,还提供了字段和构造器接入点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,无法在bean创建时应用通知。
但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用Aspect来补充Spring AOP的功能。
通过切点来选择连接点
在SpringAOP中,要使用AspectJ的切点表达式语言来定义切点。
编写切点
使用execution ()指示器选择Performance的perform()方法。方法表达式以“*”号开始,表明不关心方法返回值的类型。然后指定了全限定类名和方法名。对于方法参数列表,使用两个点好(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。
在切点中选择bean
Spring引入一个新的bean()指示器,允许在切点表达式中使用bean的ID来标识bean。
使用注解创建切面
定义切面
可以使用@Pointcut注解设置一个切点表达式。
performance ()方法的实际内容并不重要,在这里它实际上应该是空的。其实该方法本身只是一个标识,供@Pointcut注解依附。
接下来在JavaConfig的配置类级别上通过使用@EnableAspectJAutoProxy注解启动自动代理功能。
XML装配bean需要使用Spring aop。
创建环绕通知
环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。
@Around注解表明方法作为切点的环绕通知。这个通知所达到的效果与之前的前置通知和后置通知是一样的。现在它们在同一个方法中,不像之前分散。
它接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法。
别忘记调用proceed()方法,如果不调用这个方法,通知实际上会阻塞对被通知方法的调用。
处理通知中的参数
通过注解引入新功能
利用被称为引入的AOP概念,切面可以为Spring bean添加新方法。
@DeclareParents注解由三部分组成:
1.value属性指定了哪种类型的bean要引入该接口。在本例中,也就是所有实现Performance的类型。(标记符后面的加号表示是Performance的所有子类型,而不是Performance本身。)
2.defaultImpl属性指定了为引入功能提供实现的类。在这里,我们指定的是De faultEncoreable提供实现。
3.@DeclareParents注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是Encoreable接口。
在XML中声明切面
如果需要声明切面,但又不能Wie通知类添加注解时,就必须转向XML配置了。
使用aop:pointcut定义命名切点
现在切点是在一个地方定义的,并且被多个通知元素所引用。
声明环绕通知
为通知传递参数
通过切面引入新的功能
delegate-ref属性引用了一个Spring bean作为引入的委托。这需要在Spring上下文中存在一个ID为encoreableDelegate的bean。
使用default-impl来直接标识委托和间接使用delegate-ref 的区别在于后者是Spring bean,它本身可以被注人、通知或使用其他的Spring配置。
注入AspectJ切面
虽然Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比,Spring AOP是一个功能比较弱的AOP解决方案。AspectJ提供了Spring AOP所不能支持的许多类型的切点。
小结
AOP是面向对象编程的一个强大补充。通过AspectJ,我们现在可以把之前分散在应用各处的行为放人可重用的模块中。我们显示地声明在何处如何应用该行为。这有效减少了代码冗余,并让我们的类关注自身的主要功能。
Spring提供了一个AOP框架,让我们把切面插人到方法执行的周围。现在我们已经学会如何把通知织人前置、后置和环绕方法的调用中,以及为处理异常增加自定义的行为。
关于在Spring应用中如何使用切面,我们可以有多种选择。通过使用@AspectJ注解和简化的配置命名空间,在Spring中装配通知和切点变得非常简单。
最后,当Spring AOP不能满足需求时,我们必须转向更为强大的AspectJ。对于这些场景,我们了解了如何使用Spring为AspectJ切面注入依赖。